﻿using System;
using System.Collections;
using System.Linq;
using System.Reflection;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.Utilities;
using System.Collections.Generic;

namespace VNTG.UserProfileFilters
{
    /// <summary>
    /// Allows creation of exclusion filters in .NET code.
    /// Uses reflection to invoke the (hidden) internal objects/methods used by the UI to define these filters.
    /// 
    /// Each instance of this class is either targetted at "user" objects (providing filters based on user-attributes)
    /// or targetted at "group" objects (providing filters based on group-attributes).
    /// </summary>
    public class Filters
    {        
        // Typenames of the related internal classes
        string TYPENAME_FILTERSETCONDITION = "Microsoft.Office.Server.UserProfiles.Synchronization.FilterSetCondition";
        string TYPENAME_FILTERSET = "Microsoft.Office.Server.UserProfiles.Synchronization.FilterSet";
        string TYPENAME_EXCLUSIONFILTEROPERATOR = "Microsoft.Office.Server.UserProfiles.Synchronization.ExclusionFilterOperator";        
        // Location of the assembly containing these internal classes
        string UPSYNC_ASSEMBLY_PATH = @"C:\Windows\assembly\GAC_MSIL\Microsoft.Office.Server.UserProfiles.Synchronization\14.0.0.0__71e9bce111e9429c\Microsoft.Office.Server.UserProfiles.Synchronization.dll";

        Assembly _userProfileSyncAssembly;
        Type _filterSetConditionType;
        Type _filterSetType;
        Type _exclustionFilterOpType;
        string _cdObjectType;

        /// <summary>
        /// Constructor. Use it to specify whether this object manages filters based on user-related or
        /// group-related attributes.
        /// </summary>
        /// <param name="cdObjectType">
        /// Either use "user" or "group". This is the type of filter you want to create.        
        /// </param>
        /// <remarks>
        /// You can not combine user and group filters in the same set, just like in the UI.
        /// </remarks>
        public Filters(string cdObjectType)
        {
            // Get the Microsoft.Office.Server.UserProfiles.Synchronization assembly
            _userProfileSyncAssembly = Assembly.LoadFile(UPSYNC_ASSEMBLY_PATH);

            // Load required types from the assembly
            _filterSetConditionType = _userProfileSyncAssembly.GetType(TYPENAME_FILTERSETCONDITION);
            _filterSetType = _userProfileSyncAssembly.GetType(TYPENAME_FILTERSET);
            _exclustionFilterOpType = _userProfileSyncAssembly.GetType(TYPENAME_EXCLUSIONFILTEROPERATOR);

            // Set the object-type
            _cdObjectType = cdObjectType;
        }

        #region properties
        public Type FilterSetConditionType
        {
            get { return _filterSetConditionType; }
        }

        public Type FilterSetType
        {
            get { return _filterSetType; }
        }

        public Type ExclustionFilterOperatorType
        {
            get { return _exclustionFilterOpType; }
        }
        #endregion

        #region Public methods
        /// <summary>
        /// Create a new filter rule. This will generate an instance of the internal FilterSetCondition class.
        /// The IsNumeric property of this class will be determined dynamically using the connection info 
        /// and the internal DSML objects.
        /// </summary>
        /// <param name="connection">Connection from which we should retrieve the DSML properties</param>
        /// <param name="objectClass">"user" or "group", used when retrieving the related DSML attribute</param>
        /// <param name="attribute">Attribute we should filter on (e.g. "userAccountControl")</param>
        /// <param name="filterOperator">Operator to use in filter-rule (e.g. bit on equals)</param>
        /// <param name="value">Value the rule should filter on (e.g. "2")</param>
        /// <returns>An instance of the FilterSetCondition class</returns>
        public object CreateFilterSetCondition(Microsoft.Office.Server.UserProfiles.Connection connection,                                                 
                                                string attribute,
                                                FilterOperator filterOperator, 
                                                string value)
        {
            DSMLHelper dh = new DSMLHelper();
            bool isNumeric = dh.IsAttributeNumeric(connection, _cdObjectType, attribute);
            return CreateFilterSetCondition(attribute, filterOperator, value, isNumeric);
        }

        /// <summary>
        /// Create a new filter rule. This will generate an instance of the internal FilterSetCondition class.
        /// The IsNumeric property of this class is passed in this method's arguments. (no dynamic lookup using DSML)
        /// </summary>
        /// <param name="attribute">Attribute you want to filter on (e.g. "userAccountControl")</param>
        /// <param name="filterOperator">Operator to be used in filter-rule (e.g. bit on equals)</param>
        /// <param name="value">Value the rule should filter on (e.g. "2")</param>
        /// <param name="isUserAttribNumeric">Whether the attribute is numeric or not</param>
        /// <returns>An instance of the FilterSetCondition class</returns>
        public object CreateFilterSetCondition(string attribute, FilterOperator filterOperator, string value, bool isUserAttribNumeric)
        {
            // Create a new filter condition object
            var filterSetCondition = Activator.CreateInstance(FilterSetConditionType);

            // Assign its properties
            // CDAttribute
            ReflectionHelper.SetPropertyValue(filterSetCondition, FilterSetConditionType, "CDAttribute", attribute);

            // FilterOperator            
            var filterOpVal = ReflectionHelper.GetEnumValue(ExclustionFilterOperatorType, filterOperator.ToString());
            ReflectionHelper.SetPropertyValue(filterSetCondition, FilterSetConditionType, "filterOperator", filterOpVal);

            // filterSetCondition            
            if (filterOperator.Equals("Is_present") || filterOperator.Equals("Is_not_present"))
            {
                ReflectionHelper.SetPropertyValue(filterSetCondition, FilterSetConditionType, "Value", string.Empty);
            }
            else if (filterOperator.Equals("True") || filterOperator.Equals("False"))
            {
                ReflectionHelper.SetPropertyValue(filterSetCondition, FilterSetConditionType, "Value", GetOperatorString(filterOperator.ToString()));
                ReflectionHelper.SetPropertyValue(filterSetCondition, FilterSetConditionType, "filterOperator", ReflectionHelper.GetEnumValue(ExclustionFilterOperatorType, "Equals"));
            }
            else
            {
                ReflectionHelper.SetPropertyValue(filterSetCondition, FilterSetConditionType, "Value", value);
            }

            // IsNumeric 
            // Internally it only checks if the attribute's DSMLAttribute's Syntax is Integer -> if so it's true, else it's false
            //
            // UPDATE: provided an overload for this method that will use the DSML objects to check the IsNumeric value,
            // just like it's done in the internal classes
            ReflectionHelper.SetPropertyValue(filterSetCondition, FilterSetConditionType, "IsNumeric", isUserAttribNumeric);

            return filterSetCondition;
        }

        /// <summary>
        /// Create a new FilterSet object with the given conditions.        
        /// </summary>
        /// <param name="filterConditionsList">List of FilterSetCondition objects</param>
        /// <param name="andOr">
        /// AND refers to the "All apply" option in the UI, 
        /// OR refers to the "Any apply" option in the UI</param>        
        /// <returns></returns>
        public object GenerateFilterSetForConditions(IList<object> filterConditionsList, FilterSetOperator andOr)
        {   
            // Get the methodinfo for the internal static method to generate a filterset:
            // - GenerateFilterSetforOrConditions if the operator is OR
            // - GenerateFilterSetforAndConditions if the operator is AND
            MethodInfo gfsMI = null;
            if (andOr == FilterSetOperator.Or)
            {
                gfsMI = FilterSetType.GetMethod("GenerateFilterSetforOrConditions", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
            }
            else
            {
                gfsMI = FilterSetType.GetMethod("GenerateFilterSetforAndConditions", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
            }
            var filterSetObj = Activator.CreateInstance(FilterSetType);

            // Create a generic list of FilterSetCondition objects - will be passed to the internal static method in gfsMI
            Type genericList = typeof(List<>);
            var conditions = ReflectionHelper.CreateGeneric(genericList, FilterSetConditionType);

            // Dirty workaround to add items to the generic list
            IList tempCast = conditions as IList;
            foreach (var condition in filterConditionsList)
                tempCast.Add(condition);

            // Call the internal static method
            return gfsMI.Invoke(filterSetObj, new object[] { conditions, _cdObjectType });            
        }

        /// <summary>
        /// Set the array of FilterSet objects as exclusion filters on the User Profile sync-connection.
        /// </summary>
        /// <param name="connection">User profile sync connection to modify.</param>
        /// <param name="filterSets">Array of FilterSet objects (not strongly-typed as they're internal classes).</param>
        public void SetExclusionFilters(Microsoft.Office.Server.UserProfiles.Connection connection, 
                                        params object[] filterSets)
        {
            // Get a ref to the internal IsFilterValid method
            MethodInfo isfilterSetValidMI = FilterSetType.GetMethod("IsFilterSetValid", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);

            // Create a generic list of FilterSet objects
            Type genericList = typeof(List<>);
            var fsList = ReflectionHelper.CreateGeneric(genericList, FilterSetType);

            // Dirty workaround to add items to the generic list
            IList tempList = fsList as IList;
            
            // Add every valid FilterSet to our generic list
            foreach (var filterSet in filterSets)
            {
                if ((bool)isfilterSetValidMI.Invoke(filterSet, new object[] { filterSet }))
                {
                    tempList.Add(filterSet);
                }
            }

            if (tempList.Count > 0)
            {
                // Get a reference to the internal SetExclusionFilters method on the connection object
                MethodInfo setExclusionFiltersMI = connection.GetType().GetMethod("SetExclusionFilters", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

                // Bind the list of valid filtersets to this connection
                setExclusionFiltersMI.Invoke(connection, new object[] { fsList });
            }            
        }
        #endregion

        #region Private methods
        /// <summary>
        /// Reimplementation of FilterSet.GetOperatorString.
        /// </summary>
        /// <param name="conditionOperator"></param>
        /// <returns></returns>
        private string GetOperatorString(string conditionOperator)
        {
            switch (conditionOperator)
            {
                case "Equals":
                    {
                        return "equality";
                    }
                case "Does_not_equal":
                    {
                        return "inequality";
                    }
                case "Starts_with":
                    {
                        return "substring-start";
                    }
                case "Does_not_start_with":
                    {
                        return "not-substring-start";
                    }
                case "Ends_with":
                    {
                        return "substring-end";
                    }
                case "Does_not_end_with":
                    {
                        return "not-substring-end";
                    }
                case "Contains":
                    {
                        return "substring-any";
                    }
                case "Does_not_contain":
                    {
                        return "not-substring-any";
                    }
                case "Is_present":
                    {
                        return "present";
                    }
                case "Is_not_present":
                    {
                        return "not-present";
                    }
                case "Less_than":
                    {
                        return "less-than";
                    }
                case "Less_than_or_equal":
                    {
                        return "less-than-or-equal";
                    }
                case "Greater_than":
                    {
                        return "greater-than";
                    }
                case "Greater_than_or_equal":
                    {
                        return "greater-than-or-equal";
                    }
                case "Bit_on":
                    {
                        return "bit-on";
                    }
                case "Bit_off":
                    {
                        return "bit-off";
                    }
                case "True":
                    {
                        return "true";
                    }
                case "False":
                    {
                        return "false";
                    }
            }
            return "equality";
        }
        #endregion

        #region Enums
        /// <summary>
        /// Same list of filters that are available in the UI dropdowns
        /// </summary>
        public enum FilterOperator
        {
            Bit_off,
            Bit_on,
            Contains,
            Does_not_contain,
            Does_not_end_with,
            Does_not_equal,
            Does_not_start_with,
            Ends_with,
            Equals,
            False,
            Greater_than,
            Greater_than_or_equal,
            Is_not_present,
            Is_present,
            Less_than,
            Less_than_or_equal,
            Starts_with,
            True
        }

        public enum FilterSetOperator
        {
            And,
            Or
        }
        #endregion
    }
}
